package nl.vu.few.ibivu.biomodels.server;

import java.util.logging.Logger;

import nl.vu.few.ibivu.biomodels.client.GwtRpcResultList;
import nl.vu.few.ibivu.biomodels.client.ModelService;
import nl.vu.few.ibivu.biomodels.server.petrinet.PetrifierExecutionTask;
import nl.vu.few.ibivu.biomodels.shared.Model;
import nl.vu.few.ibivu.biomodels.shared.NotFindException;
import nl.vu.few.ibivu.biomodels.shared.NotLoggedInException;
import nl.vu.few.ibivu.biomodels.shared.OperationNotPermittedException;
import nl.vu.few.ibivu.biomodels.shared.PetrifierModelExecution;
import nl.vu.few.ibivu.biomodels.shared.UserProfile;

import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.appengine.api.images.Image;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.appengine.api.images.Transform;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.RetryOptions;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.repackaged.com.google.common.util.Base64;
import com.google.appengine.repackaged.com.google.common.util.Base64DecoderException;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.Query;

@SuppressWarnings("serial")
public class ModelServiceImpl extends RemoteServiceServlet implements ModelService {
	private static final Logger log = Logger.getLogger(ModelServiceImpl.class.getName());
	
	private Model find(Objectify ofy, Long modelId) throws NotFindException {
		Model model = ofy.find(Model.class, modelId);
		if (model == null)
			throw new NotFindException("Model " + modelId + " not found!");
		model.setOwner(getModelOwner(ofy, model));
		return model;
	}
	
	private void delete(Objectify ofy, UserProfile current, Model model) throws OperationNotPermittedException{
		if (model.getId() == null) {
			// the model has not been saved yet?!
			throw new IllegalStateException();
		}
		UserProfile owner = getModelOwner(ofy, model);
		if (owner == null) {
			// the model is not owned by anyone?!
			throw new IllegalStateException();
		}
		// check if the model doesn't belongs to the current user.
		if (owner.getId() != current.getId()) {
			// you can't remove other people models
			throw new OperationNotPermittedException("The model belongs to someone else.");
		}
		
		// delete execution
		try {
			ofy.delete(getModelExecution(ofy, new Key<Model>(Model.class, model.getId())));
		} catch (NotFindException ignored) {}
		
		ofy.delete(model);
	}
	
	private PetrifierModelExecution getModelExecution(Objectify ofy, Key<Model> model) throws NotFindException {
		PetrifierModelExecution execution;
		
		Query<PetrifierModelExecution> qry = ofy.query(PetrifierModelExecution.class).filter("model", model);
		if ((execution = qry.get()) == null)
			throw new NotFindException("Model execution not found for model " + model.getId());
		return execution;
	}
	
	private Integer countUserModels(Objectify ofy, UserProfile current) {
		return ofy.query(Model.class)
			.filter("owner", current)
			.count();
	}
	
	private UserProfile getModelOwner(Objectify ofy, Model model) {
		UserProfile owner = null;
		if (model.getOwnerKey() == null) {
			owner = null;
		}
		else {
			owner = ofy.find(model.getOwnerKey());
		}
		return owner;
	}
	
	@Override
	public Model fetch(Long modelId) throws NotFindException {
		DAO dao = new DAO();
		return find(dao.ofy(), modelId);
	}
	
	@Override
	public Integer count() throws NotLoggedInException{
		DAO dao = new DAO();
		return countUserModels(dao.ofy(), dao.getOrCreateUserProfile());
	}
	
	@Override
	public GwtRpcResultList<Model> fetch(String cursor, Integer pageSize, String order)
			throws NotLoggedInException {
		DAO dao = new DAO();
		UserProfile userProfile = dao.getOrCreateUserProfile();
		Objectify ofy = dao.ofy();
		
		Query<Model> qry = ofy.query(Model.class);
		if (cursor != null) {
			qry.startCursor(Cursor.fromWebSafeString(cursor));
		}
		else {
			qry.filter("owner", userProfile);
			if (order != null)
				qry.order(order);
		}
		if (pageSize != null)
			qry.limit(pageSize);
		
		QueryResultIterator<Model> qri = qry.iterator();
		GwtRpcResultList<Model> result = new GwtRpcResultList<Model>(qri);
		for (Model model : result) {
			model.setOwner(userProfile);
		}
		result.setTotalRows(countUserModels(ofy, userProfile));
		result.setNextCursor(qri.getCursor().toWebSafeString());
		return result;
	}
	
	@Override
	public GwtRpcResultList<Model> fetch(Integer startRow, Integer endRow, String order)
			throws NotLoggedInException {
		DAO dao = new DAO();
		UserProfile userProfile = dao.getOrCreateUserProfile();
		Objectify ofy = dao.ofy();
		
		Query<Model> qry = ofy.query(Model.class).filter("owner", userProfile);
		if (startRow != null)
			qry.offset(startRow);
		if (endRow != null)
			qry.limit(endRow);
		if (order != null)
			qry.order(order);
		
		QueryResultIterator<Model> qri = qry.iterator();
		GwtRpcResultList<Model> result = new GwtRpcResultList<Model>(qri);
		for (Model model : result) {
			model.setOwner(userProfile);
		}
		result.setTotalRows(countUserModels(ofy, userProfile));
		result.setNextCursor(qri.getCursor().toWebSafeString());
		return result;
	}
	
	@Override
	public Model save(Model model) throws NotLoggedInException, OperationNotPermittedException {
		DAO dao = new DAO();
		UserProfile userProfile = dao.getOrCreateUserProfile();
		Objectify ofy = dao.ofy();
		UserProfile owner = getModelOwner(ofy, model);
		if (owner == null) {
			if (model.getId() != null) {
				// the model is *supposedly* new but it isn't
				throw new IllegalStateException();
			}
			// the model is *supposedly* new therefore associate the owner
			model.setOwner(userProfile);
			owner = userProfile;
		}
		// check if the model doesn't belongs to the current user
		if (owner.getId() != userProfile.getId()) {
			// you can't save other people models
			throw new OperationNotPermittedException("The model belongs to someone else.");
		}
		
		// thumbnail image resizing
		String image = model.getThumbnail();
		if (image != null) {
			ImagesService imagesService = ImagesServiceFactory.getImagesService();
			String newImage = null;
			try {
				Image fullImage = ImagesServiceFactory.makeImage(Base64.decode(image));
				int newW = fullImage.getWidth();
				int newH = fullImage.getHeight();
				// if the W side is the wider
				if (newW >= newH) {
					// check if is small enough
					if (newW <= 150) {
						newImage = image;
					}
					// else, set the longest side (W) to 150 and
					// reduce the smaller side (H) maintaining proportions
					else {
						newH = newH * 150 / newW;
						newW = 150;
					}
				}
				// if the H side is the wider to the opposite
				else {
					if (newH <= 150) {
						newImage = image;
					}
					else {
						newW = newW * 150 / newH;
						newH = 150;
					}
				}
				// if the image need to be resized to so
				if (newImage == null) {
					Transform resize = ImagesServiceFactory.makeResize(150, 
							fullImage.getHeight() * 150 / fullImage.getWidth());
					Image smallImage = imagesService.applyTransform(resize, fullImage);
					newImage = Base64.encode(smallImage.getImageData());
				}
			} catch (Base64DecoderException e) {
				log.warning("Base64DecoderException: " + e.getMessage());
			}
			finally {
				model.setThumbnail(newImage);
			}
		}
		
		// if it is not a new model, then delete the available execution
		if (model.getId() != null) {
			try {
				ofy.delete(getModelExecution(ofy, new Key<Model>(Model.class, model.getId())));
			} catch (NotFindException ignored) {}
		}
		
		ofy.put(model);
		return model;
	}

	@Override
	public void remove(Long modelId) throws NotLoggedInException, NotFindException,
			OperationNotPermittedException {
		DAO dao = new DAO();
		Objectify ofy = new DAO().ofy();
		delete(ofy, dao.getOrCreateUserProfile(), find(ofy, modelId));
	}
	
	@Override
	public void execute(Long modelId) throws NotLoggedInException, NotFindException, OperationNotPermittedException {
		DAO dao = new DAO();
		Objectify ofy = dao.ofy();
		
		// check if the model has been already executed
		if (ofy.query(PetrifierModelExecution.class)
				.filter("model", new Key<Model>(Model.class, modelId))
				.count() > 0) {
			throw new OperationNotPermittedException("The model has been already executed.");
		}
		
		Model model = find(ofy, modelId);
		
		UserProfile userProfile = dao.getOrCreateUserProfile();
		UserProfile owner = getModelOwner(ofy, model);
		
		if (owner == null) {
			// the model is not owned by anyone?!
			throw new IllegalStateException();
		}
		// check if the model doesn't belongs to the current user.
		if (owner.getId() != userProfile.getId()) {
			// you can't remove other people models
			throw new OperationNotPermittedException("The model belongs to someone else.");
		}
		
		QueueFactory.getDefaultQueue().add(TaskOptions.Builder.withPayload(new PetrifierExecutionTask(model)).retryOptions(RetryOptions.Builder.withTaskRetryLimit(1)));
	}
	
	@Override
	public PetrifierModelExecution fetchExecution(Long modelId) throws NotFindException, NotLoggedInException, OperationNotPermittedException {
		DAO dao = new DAO();
		Objectify ofy = new DAO().ofy();
		
		// get the model
		Model model = find(ofy, modelId);
		
		UserProfile userProfile = dao.getOrCreateUserProfile();
		UserProfile owner = getModelOwner(ofy, model);
		if (owner == null) {
			// the model is not owned by anyone?!
			throw new IllegalStateException();
		}
		// check if the model doesn't belongs to the current user.
		if (owner.getId() != userProfile.getId()) {
			// you can't remove other people models
			throw new OperationNotPermittedException("The model belongs to someone else.");
		}
		
		return getModelExecution(ofy, new Key<Model>(Model.class, modelId));
	}
	
	@Override
	public boolean isExecuted(Long modelId) {
		return new DAO().ofy().query(PetrifierModelExecution.class)
			.filter("model", new Key<Model>(Model.class, modelId))
			.count() > 0;
	}
	

	
}
